package com.j01y.myhooks.Entry;

import android.app.Application;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;

import com.j01y.myhooks.MyApplication;
import com.j01y.myhooks.Utils.HookHelpers;
import com.j01y.myhooks.Utils.Logger;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import dalvik.system.PathClassLoader;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;


/**
 * 第一层Hook入口
 */
public class BaseEntry implements IXposedHookLoadPackage {
    private static final String modulePackage = MyApplication.class.getPackage().getName();  // 动态获取当前打开的xpposed模块的包名
    private static List<String> hookPackages = null;  // 要Hook的包名数组
    private static final String handleHookClass = RealEntry.class.getName();
    public static Context appContext;

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
        Logger.logi("test");
        try {
            // 获取当前xposed模块的context对象
            Context context = HookHelpers.hookContext(lpparam);

            //根据配置文件设置需要hook的包名
            setNeedHookPackage(context);

            if ((hookPackages != null) && (hookPackages.contains(lpparam.packageName)) && (!lpparam.processName.equals(modulePackage))) {
                // Hook获取app的classloader
                try {
                    XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            super.afterHookedMethod(param);
                            Logger.logi(String.format("Get Needed Hook Package:%s", lpparam.packageName));
                            appContext = (Context) param.args[0];
                            lpparam.classLoader = appContext.getClassLoader();  // 替换原有的classLoader
                            //反射调用真正hook逻辑类的handleLoadPackage函数
                            invokeHandleHookMethod(appContext, lpparam);
                        }
                    });
                } catch (Exception e) {
                    Logger.loge(e.toString());
                }
            }
        } catch (Exception e) {
            Logger.loge(e.toString());
        }
    }

    /**
     * @param appContext       当前hook到的app的context
     * @param loadPackageParam 当前hook到的app的loadPackageParam
     */
    private void invokeHandleHookMethod(Context appContext, XC_LoadPackage.LoadPackageParam loadPackageParam) {
        try {
            // 同样的寻包过程
            File apkFile = findApkFile(appContext);
            if (apkFile == null) {
                throw new RuntimeException("Find Apk File Error");
            }
            Logger.logi(String.format("Get Apk File:%s", apkFile.toString()));

            // 新建pathclassloader得到真正hook逻辑类，并调用hook方法
            PathClassLoader pathClassLoader = new PathClassLoader(apkFile.getAbsolutePath(), XposedBridge.BOOTCLASSLOADER);
            Logger.logi(String.format("get pathClassLoader:%s",pathClassLoader));

            Class<?> cls = Class.forName(handleHookClass, true, pathClassLoader); // 真正进行hook的入口类，也就是RealEntry
            Logger.logi(String.format("Get real hook enter class:%s", cls));

            Method declaredMethod = cls.getDeclaredMethod( "handleLoadPackage", XC_LoadPackage.LoadPackageParam.class);
            Logger.logi(String.format("Get Needed Hook Method:%s", declaredMethod.getName()));

            declaredMethod.invoke(cls.newInstance(), loadPackageParam);  // 调用RealEntry实例的handleLoadPackage方法，开始真正的hook工作
        } catch (Exception e) {
            Logger.loge(e.toString());
        }


    }

    /**
     * 传入插件包名，返回插件apk的File对象
     *
     * @param context 当插件的context
     * @return 返回File对象
     */
    private File findApkFile(Context context) {
        if (context == null) {
            throw new RuntimeException("Can't Get Context");
        }
        try {
            // 创建xposed模块的context
            Context moduleContext = context.createPackageContext(BaseEntry.modulePackage, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
            // 获取模块的代码路径
            String apkPath = moduleContext.getPackageCodePath();
            return new File(apkPath);
        } catch (PackageManager.NameNotFoundException e) {
            Logger.loge(String.format("Find File Error，Package:%s", BaseEntry.modulePackage));
            return null;
        }
    }


    /**
     * 从xposed模块zip包的assets/config文件读取需要hook的包名
     *
     * @param context 当前插件的context
     */
    private void setNeedHookPackage(Context context) {
        ArrayList<String> NeedHookPackages = new ArrayList<String>();
        try {
            String apkPath = findApkFile(context).toString();

            // 简单暴力使用zip来解包获取config文件，之前采用getSource发现加入免重启功能后导致获取原包路径失败，因此换用这种方案
            ZipFile apkZipFile = new ZipFile(apkPath);
            ZipEntry apkFileEntry = apkZipFile.getEntry("assets/config");

            // 读取数据流转换ArrayList
            InputStream inputStream = apkZipFile.getInputStream(apkFileEntry);
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                if (line.contains(".name")) {
                    String[] split = line.split("=");
                    NeedHookPackages.add(split[1].trim());
                }
            }
            Logger.logi("NeedHookPackages:" + NeedHookPackages.toString());
            hookPackages = NeedHookPackages;
        } catch (Exception e) {
            Logger.loge(e.toString());
        }


    }
}
